from flask import Flask, render_template, request, send_from_directory, jsonify, Response, send_file, abort
import os
import socket
import json
import subprocess
import hashlib
import shutil
from werkzeug.utils import secure_filename
from concurrent.futures import ThreadPoolExecutor
from functools import partial
import uuid
import time

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
# app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 1024  # 1GB限制

# 初始化上传目录
if not os.path.exists(app.config['UPLOAD_FOLDER']):
    os.makedirs(app.config['UPLOAD_FOLDER'])
if not os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], 'chunks')):
    os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], 'chunks'))

# 线程池用于异步转码等后台任务
executor = ThreadPoolExecutor(max_workers=2)


def get_local_ip():
    """获取本地局域网IP"""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return "127.0.0.1"


def convert_to_mp4(input_path, output_path):
    try:
        subprocess.run([
            'ffmpeg', '-i', input_path,
            '-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
            '-c:a', 'aac', '-b:a', '128k',
            '-y',    # 强制覆盖输出文件
            output_path
        ], check=True)
        return True
    except subprocess.CalledProcessError:
        return False


def unique_filename(original_name: str) -> str:
    """生成一个不重复的文件名，保留原始后缀，使用时间戳+UUID保证唯一性"""
    file_name_without_ext, ext = os.path.splitext(original_name)
    print(f"Generating unique filename for {original_name} with extension {ext}")
    ts = int(time.time())
    uid = uuid.uuid4().hex
    return f"{file_name_without_ext}_{ts}{ext}"


def cleanup_old_chunks(hours=24):
    """清理 uploads/chunks 下比 hours 更早的临时分块目录，避免占用过多磁盘空间。
    通过修改 hours 参数可调整保留时长。
    """
    base = os.path.join(app.config['UPLOAD_FOLDER'], 'chunks')
    if not os.path.exists(base):
        return
    now = os.path.getmtime(__file__)
    try:
        now = os.path.getmtime(__file__)
    except Exception:
        now = None
    # 使用时间阈值（以当前时间为准）
    import time
    cur = time.time()
    threshold = cur - hours * 3600
    for name in os.listdir(base):
        path = os.path.join(base, name)
        try:
            mtime = os.path.getmtime(path)
            if mtime < threshold:
                shutil.rmtree(path)
        except Exception:
            # 忽略删除错误
            pass


def calc_md5(path, chunk_size=8 * 1024 * 1024):
    """计算文件的 MD5"""
    h = hashlib.md5()
    with open(path, 'rb') as f:
        for chunk in iter(lambda: f.read(chunk_size), b''):
            h.update(chunk)
    return h.hexdigest()


def partial_response(path):
    """支持 HTTP Range 的响应，用于断点续传下载和视频拖拽播放"""
    file_size = os.path.getsize(path)
    range_header = request.headers.get('Range', None)
    if not range_header:
        return send_file(path, as_attachment=True)

    # Range: bytes=start-end
    try:
        bytes_range = range_header.split('=')[1].strip()
        if ',' in bytes_range:
            # 多段不支持，返回 416
            return Response(status=416)
        start_str, end_str = bytes_range.split('-')
        start = int(start_str) if start_str else 0
        end = int(end_str) if end_str else file_size - 1
    except Exception:
        return Response(status=416)

    if start >= file_size:
        return Response(status=416)

    end = min(end, file_size - 1)
    length = end - start + 1

    def generate():
        with open(path, 'rb') as f:
            f.seek(start)
            remaining = length
            chunk_size = 8192
            while remaining > 0:
                read_len = min(chunk_size, remaining)
                data = f.read(read_len)
                if not data:
                    break
                remaining -= len(data)
                yield data

    rv = Response(generate(), status=206, mimetype='application/octet-stream')
    rv.headers['Content-Range'] = f'bytes {start}-{end}/{file_size}'
    rv.headers['Accept-Ranges'] = 'bytes'
    rv.headers['Content-Length'] = str(length)
    return rv

@app.route('/')
def index():
    """主页面：显示上传表单和文件列表"""
    files = [f for f in os.listdir(app.config['UPLOAD_FOLDER'])
             if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f))]
    return render_template('index.html', files=files, ip=get_local_ip())


@app.route('/upload', methods=['POST'])
def upload_file():
    """兼容整文件上传（保留），改为异步转码以避免阻塞请求。推荐使用分块接口。"""
    uploaded_files = request.files.getlist('file')  # 获取所有上传的文件
    if not uploaded_files:
        return jsonify({"status": "error", "msg": "未选择文件"})

    success_files = []
    for file in uploaded_files:
        if file.filename == '':
            continue
        orig_name = secure_filename(file.filename)
        unique_name = unique_filename(orig_name)
        save_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_name)
        file.save(save_path)

        # 异步转码（保留原始文件后替换）
        if unique_name.lower().endswith(('.mov', '.hevc', '.h265', '.avi', '.webm', '.mkv')):
            executor.submit(async_transcode_task, save_path)

        success_files.append(unique_name)

    if not success_files:
        return jsonify({"status": "error", "msg": "所有文件上传失败（空文件名）"})
    return jsonify({
        "status": "success",
        "msg": f"成功上传{len(success_files)}个文件",
        "filenames": success_files
    })


@app.route('/upload/init', methods=['POST'])
def upload_init():
    """初始化分块上传：客户端提交 fileName, fileMd5, totalChunks"""
    data = request.form or request.json or {}
    file_name = data.get('fileName') or request.form.get('fileName')
    file_md5 = data.get('fileMd5') or request.form.get('fileMd5')
    total = int(data.get('totalChunks') or request.form.get('totalChunks') or 0)
    if not file_name or not file_md5 or total <= 0:
        return jsonify({'status': 'error', 'msg': '参数不完整（fileName,fileMd5,totalChunks）'}), 400

    # 清理旧的临时分块（默认 24 小时以前的）以避免长期占用磁盘
    try:
        cleanup_old_chunks(hours=24)
    except Exception:
        pass

    # 创建分块目录
    chunk_dir = os.path.join(app.config['UPLOAD_FOLDER'], 'chunks', file_md5)
    os.makedirs(chunk_dir, exist_ok=True)

    # 返回已上传分块列表以便断点续传
    uploaded = [int(name.split('.')[0]) for name in os.listdir(chunk_dir) if name.endswith('.chunk') and name.split('.')[0].isdigit()]
    return jsonify({'status': 'ok', 'uploaded': uploaded})


@app.route('/upload/chunk', methods=['POST'])
def upload_chunk():
    """接收单个分块：字段 fileMd5, index, chunk (文件块)"""
    file_md5 = request.form.get('fileMd5')
    index = request.form.get('index')
    chunk = request.files.get('chunk')
    if not file_md5 or index is None or chunk is None:
        return jsonify({'status': 'error', 'msg': '参数不完整（fileMd5,index,chunk）'}), 400

    try:
        idx = int(index)
    except Exception:
        return jsonify({'status': 'error', 'msg': 'index 必须为整数'}), 400

    chunk_dir = os.path.join(app.config['UPLOAD_FOLDER'], 'chunks', file_md5)
    os.makedirs(chunk_dir, exist_ok=True)
    chunk_path = os.path.join(chunk_dir, f"{idx}.chunk")
    chunk.save(chunk_path)
    return jsonify({'status': 'ok', 'msg': 'chunk saved', 'index': idx})


@app.route('/upload/merge', methods=['POST'])
def upload_merge():
    """合并分块并校验 md5：fileName, fileMd5, totalChunks"""
    data = request.form or request.json or {}
    file_name = data.get('fileName') or request.form.get('fileName')
    file_md5 = data.get('fileMd5') or request.form.get('fileMd5')
    total = int(data.get('totalChunks') or request.form.get('totalChunks') or 0)
    if not file_name or not file_md5 or total <= 0:
        return jsonify({'status': 'error', 'msg': '参数不完整（fileName,fileMd5,totalChunks）'}), 400

    chunk_dir = os.path.join(app.config['UPLOAD_FOLDER'], 'chunks', file_md5)
    if not os.path.exists(chunk_dir):
        return jsonify({'status': 'error', 'msg': '分块目录不存在'}), 400

    # 目标文件使用唯一命名，保留原始后缀
    orig_target = secure_filename(file_name)
    unique_name = unique_filename(orig_target)
    target_filename = unique_name
    target_path = os.path.join(app.config['UPLOAD_FOLDER'], target_filename)

    # 合并
    with open(target_path + '.part', 'wb') as wf:
        for i in range(total):
            part_path = os.path.join(chunk_dir, f"{i}.chunk")
            if not os.path.exists(part_path):
                return jsonify({'status': 'error', 'msg': f'缺失分块 {i}'}), 400
            with open(part_path, 'rb') as rf:
                shutil.copyfileobj(rf, wf)

    # 计算合并后的文件 MD5 并比对
    real_md5 = calc_md5(target_path + '.part')
    if real_md5 != file_md5:
        # 移除临时文件
        os.remove(target_path + '.part')
        return jsonify({'status': 'error', 'msg': '合并后 MD5 校验失败', 'expected': file_md5, 'actual': real_md5}), 400

    # 合并成功，重命名为最终文件
    os.rename(target_path + '.part', target_path)

    # 清理临时分块
    try:
        shutil.rmtree(chunk_dir)
    except Exception:
        pass

    # 清理超过阈值的旧临时分块
    try:
        cleanup_old_chunks(hours=24)
    except Exception:
        pass

    # 异步提交转码任务（如果是视频）
    if target_filename.lower().endswith(('.mov', '.hevc', '.h265', '.avi', '.webm', '.mkv')):
        executor.submit(async_transcode_task, target_path)

    return jsonify({'status': 'ok', 'msg': 'merge ok', 'filename': target_filename})


@app.route('/upload/check', methods=['GET'])
def upload_check():
    """检查是否已上传或返回已上传的分块索引：?fileMd5=...&fileName=..."""
    file_md5 = request.args.get('fileMd5')
    file_name = request.args.get('fileName')
    if not file_md5:
        return jsonify({'status': 'error', 'msg': 'missing fileMd5'}), 400

    chunk_dir = os.path.join(app.config['UPLOAD_FOLDER'], 'chunks', file_md5)
    if not os.path.exists(chunk_dir):
        return jsonify({'status': 'ok', 'uploaded': []})

    uploaded = [int(name.split('.')[0]) for name in os.listdir(chunk_dir) if name.endswith('.chunk') and name.split('.')[0].isdigit()]
    return jsonify({'status': 'ok', 'uploaded': uploaded})


@app.route('/external/upload', methods=['POST'])
def external_upload():
    """对外统一上传入口（兼容大/小文件）：
    - 若请求包含表单字段 action=merge -> 触发合并 (调用 /upload/merge)
    - 若请求包含分块字段 index 和 chunk -> 触发分块上传 (调用 /upload/chunk)
    - 若请求包含 file -> 触发整文件上传 (调用 /upload)

    这样外部系统只需调用一个接口，根据需要上传单个完整文件或逐块上传并最后发起合并。
    返回值与对应内部接口一致（JSON）。
    """
    # 优先判断合并动作
    action = request.form.get('action') or request.args.get('action')
    if action == 'merge':
        return upload_merge()

    # 如果包含分块字段则当作 chunk 上传
    if request.files.get('chunk') is not None or request.form.get('index') is not None:
        return upload_chunk()

    # 否则如果包含 file 字段则当作整文件上传（兼容旧客户端）
    if request.files.getlist('file'):
        return upload_file()

    return jsonify({'status': 'error', 'msg': '不支持的请求格式：请提供 file 或 chunk/index，或 action=merge'}), 400


@app.route('/download/<filename>')
def download_file(filename):
    """处理文件下载"""
    file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if not os.path.exists(file_path):
        return "文件不存在", 404
    return partial_response(file_path)

@app.route('/stream/<filename>')
def stream_file(filename):
    """处理视频流播放"""
    file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if not os.path.exists(file_path):
        return "文件不存在", 404
    
    # 设置正确的MIME类型
    mime_types = {
        '.mp4': 'video/mp4',
        '.mov': 'video/quicktime',
        '.hevc': 'video/mp4',  # HEVC通常封装在MP4容器中
        '.h265': 'video/mp4',  # H.265通常封装在MP4容器中
        '.avi': 'video/x-msvideo',
        '.webm': 'video/webm',
        '.mkv': 'video/x-matroska'
    }
    
    _, ext = os.path.splitext(filename)
    mime_type = mime_types.get(ext.lower(), 'application/octet-stream')
    file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if not os.path.exists(file_path):
        return "文件不存在", 404
    # 使用 partial_response 返回支持 Range 的响应，并设置 mime
    resp = partial_response(file_path)
    # override mimetype when it's a full send_file Response
    resp.headers['Content-Type'] = mime_type
    return resp

def to_nice_json(value):
    try:
        obj = json.loads(value)
        return json.dumps(obj, indent=4, ensure_ascii=False)
    except:
        return value

app.jinja_env.filters['to_nice_json'] = to_nice_json

@app.route('/preview/<filename>')
def preview_file(filename):
    file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if not os.path.exists(file_path):
        return render_template('preview.html', filename=filename, error="文件不存在", content=None)

    _, ext = os.path.splitext(filename)
    ext = ext.lower()

    video_exts = ['.mp4', '.mov', '.hevc', '.h265', '.avi', '.webm', '.mkv']

    if ext in ['.txt', '.json']:
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
            return render_template('preview.html', filename=filename, content=content, file_type=ext[1:])
        except Exception as e:
            return render_template('preview.html', filename=filename, error=f"无法读取文件内容: {str(e)}", content=None)
    elif ext in video_exts:
        return render_template('preview.html', filename=filename, content=filename, file_type='video')
    else:
        return render_template('preview.html', filename=filename, error=f"不支持预览该文件类型 ({ext})", content=None)

@app.route('/deletefiles')
def delete_files():
    """全部文件删除"""
    files = [f for f in os.listdir(app.config['UPLOAD_FOLDER'])
             if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f))]
    for file in files:
        os.remove(os.path.join(app.config['UPLOAD_FOLDER'], file))

    return jsonify({
        "status": "success",
        "msg": f"成功删除{len(files)}个文件"
    })

@app.route('/delete/<filename>')
def delete_file(filename):
    """单个文件删除"""
    files = [f for f in os.listdir(app.config['UPLOAD_FOLDER'])
             if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f))]
    for file in files:
        if file == filename:
            os.remove(os.path.join(app.config['UPLOAD_FOLDER'], file))

    return jsonify({
        "status": "success",
        "msg": f"成功删除{file}文件"
    })


def async_transcode_task(src_path):
    """后台转码任务：先保存原始文件为 .orig，转码成功则替换"""
    try:
        orig_path = src_path + '.orig'
        if not os.path.exists(orig_path):
            shutil.copy2(src_path, orig_path)

        out_path = src_path + '.converted.mp4'
        success = convert_to_mp4(src_path, out_path)
        if success:
            # 替换文件（保留原始 .orig）
            os.replace(out_path, src_path)
    except Exception:
        pass


if __name__ == '__main__':
    ip = get_local_ip()
    port = 5000
    print(f"服务器已启动，局域网内其他设备访问：http://{ip}:{port}")
    app.run(host='0.0.0.0', port=port, debug=True)